A Brief Discussion on the Application and Insights of Flag Enums
What is an Enum
Before discussing Flag Enums, let's first understand what an Enum (enumeration type) is. An Enum is a value type composed of a set of named integer constants. The default underlying type for enumeration members is int, but it can be specified as other integer types. If the value of an enumeration member is not specified, it starts at 0 and increments by one.
In software development, it is common to use a set of constants to represent specific states, types, or operations, especially when used as method parameters. While using numeric values can reduce the possibility of input errors, it is not intuitive to identify the specific meaning of each value. Although using meaningful strings can reduce the possibility of string input errors, defining constants for each value individually helps improve this issue but still cannot prevent the use of other undefined values.
Through Enums, we can assign meaningful names to each enumeration member, which improves code readability while preventing the possibility of using undefined values, thereby increasing the stability of the program. Even for developers who are not familiar with English and cannot understand the naming of Enum members, they can still clearly understand the meaning of each item through the comments displayed by the editor.
The following is an Enum example:
enum Action : ushort { // Specify the member type as ushort; defaults to int if not specified
/// <summary>
/// None(0)
/// </summary>
None, // No value set, starts from 0
/// <summary>
/// Query(1)
/// </summary>
Query, // Value is 1
/// <summary>
/// Create(10)
/// </summary>
Create = 10,
/// <summary>
/// Update(11)
/// </summary>
Update, // Since the previous item is set to 10, the value is 11
/// <summary>
/// Delete(12)
/// </summary>
Dalete // Value is 12
}
Action action = Action.Update;
string name = action.ToString(); // name = "Update"
ushort value = (ushort)action; // value = 11Introduction to Flag Enums
A Flag Enum can be viewed as a set of Enums that support bitwise operations, where each enumeration value represents an independent flag. By combining these flags, you can effectively represent multiple states or options.
How to Define a Flag Enum
A Flag Enum is defined by adding the FlagsAttribute to the Enum and using powers of 2 to define each enumeration value. Other values are composite values, and you can also define your own composite enumeration members.
The following is an example:
[Flags]
enum Permissions {
None = 0,
CanQuery = 1,
CanCreate = 2,
CanUpdate = 4,
CanDelete = 8,
CanUpsert = CanCreate | CanUpdate, // Value is 6
ExcludeDelete = ~CanDelete, // Value is -9
All = CanQuery | CanCreate | CanUpdate | CanDelete
}
// Directly use the defined composite value; the ToString() result is the name of that value
Permissions permission = Permissions.CanUpsert;
string name = permission.ToString(); // name = "CanUpsert"
int value = (int)permission; // value = 6
// Use bitwise operations; the result is the defined composite value, and ToString() returns the name of that value
permission = Permissions.CanCreate | Permissions.CanUpdate;
name = permission.ToString(); // name = "CanUpsert"
value = (int)permission; // value = 6
// Use bitwise operations; the result is a non-defined composite value, and ToString() returns the names of the individual enumeration values
permission = Permissions.CanCreate | Permissions.CanDelete;
name = permission.ToString(); // name = "CanCreate, CanDelete"
value = (int)permission; // value = 10
// Use the ~ bitwise operation; the result is the defined composite value, and ToString() returns the name of that value
permission = ~Permissions.CanDelete;
name = permission.ToString(); // name = "ExcludeDelete"
value = (int)permission; // value = -9
// Use the ~ bitwise operation; the result is a non-defined composite value, and ToString() returns the numeric value
permission = ~Permissions.CanCreate;
name = permission.ToString(); // name = "-3"
value = (int)permission; // value = -3Or define values using bitwise operations:
enum Permissions {
None = 0,
CanQuery = 1 << 0,
CanCreate = 1 << 1,
CanUpdate = 1 << 2,
CanDelete = 1 << 3
}Microsoft recommends using plural names for Flag Enum types, such as RegexOptions; and singular names for general Enum types, such as DayOfWeek.
How to Use Flag Enums
Flag Enums can effectively simplify method parameters, making them more readable. The following case is very suitable for refactoring using a Flag Enum:
void Execute(bool canQuery, bool canCreate, bool canUpdate, bool canDelete) {
// Actual execution logic
}Using the Flag Enum approach:
void Execute(Permissions permiss) {
// Actual execution logic
}This refactoring makes the method parameters clearer and eliminates the potential confusion when using multiple boolean values. Using Flag Enums not only improves readability but also makes it more convenient to extend permissions in the future.
Bitwise Operations
The following uses the concept of sets to explain bitwise operations related to Flag Enums.
- OR (
|) operator: Performs an OR operation on two enumeration values to form a set containing both enumeration items, i.e., a union.
- AND (
&) operator: Performs an AND operation on two enumeration values to form a set containing the overlapping enumeration items, i.e., an intersection.
- XOR (
^) operator: Performs an XOR operation on two enumeration values to form a set that does not contain the overlapping items, i.e., a symmetric difference.
- NOT (
~) operator: Performs a NOT operation on an enumeration value to produce a set of enumeration values that does not contain the original items, i.e., a complement.
There is no difference operator for bitwise operations, so you cannot simply remove a specific enumeration item. However, you can achieve the same effect using the following methods:
- Get the complement of the item to be removed, then perform an intersection with the original item. The code is
Permissions.CanUpsert & ~Permissions.CanCreate. - Form a union with the item to be removed, then perform a symmetric difference with the item to be removed. The code is
(Permissions.CanUpsert | Permissions.CanCreate) ^ Permissions.CanCreate.
Determining if a Specific Enumeration Value is Included
To determine if a specific enumeration item is included, you can use the following approaches:
- Perform an intersection with the enumeration item to be checked, then check for equality.
// has1 = true
bool has1 = (Permissions.CanUpsert & Permissions.CanCreate) == Permissions.CanCreate;
// has2 = false
bool has2 = (Permissions.CanUpsert & Permissions.CanDelete) == Permissions.CanDelete;
// has3 = true
bool has3 = (Permissions.CanUpsert & Permissions.None) == Permissions.None;
// has4 = false
bool has4 = (Permissions.ExcludeDelete & Permissions.CanDelete) == Permissions.CanDelete;
// has5 = true
bool has5 = (Permissions.ExcludeDelete & Permissions.CanCreate) == Permissions.CanCreate;
// has6 = false
bool has6 = (Permissions.All & Permissions.ExcludeDelete) == Permissions.ExcludeDelete;HasFlag: A method added to Enum in .NET Framework 4.0. It internally uses the logic described above. The results are as follows:
// has1 = true
bool has1 = Permissions.CanUpsert.HasFlag(Permissions.CanCreate);
// has2 = false
bool has2 = Permissions.CanUpsert.HasFlag(Permissions.CanDelete);
// has3 = true
bool has3 = Permissions.CanUpsert.HasFlag(Permissions.None);
// has4 = false
bool has4 = Permissions.ExcludeDelete.HasFlag(Permissions.CanDelete);
// has5 = true
bool has5 = Permissions.ExcludeDelete.HasFlag(Permissions.CanCreate);
// has6 = false
bool has6 = Permissions.All.HasFlag(Permissions.ExcludeDelete);Concerns Regarding Bitwise Operations
In the examples above, you might have concerns about Permissions.CanUpsert.HasFlag(Permissions.None) and Permissions.All.HasFlag(Permissions.ExcludeDelete).
First, from the perspective of bitwise operations, Permissions.CanUpsert & Permissions.None == Permissions.None being true is correct. From the perspective of sets, None represents an empty set, and an empty set is a subset of any set. Even if None is defined, the result remains as shown in the figure below:
Rather than the figure below:
The concerns regarding Permissions.All.HasFlag(Permissions.ExcludeDelete) mainly stem from the naming of All and a potential misunderstanding of ExcludeDelete. Although named All, it actually only contains the defined enumeration items. In other words, if a new enumeration item is defined but the value of All is not updated, it will not contain the newly defined value. Therefore, for Enums that might be extended, you should be cautious about using the name All. ExcludeDelete is defined using the NOT (~) operator; it consists of defined enumeration items that do not include Delete and undefined values. This is shown in the orange area in the figure below:
Therefore, the result of Permissions.All.HasFlag(Permissions.ExcludeDelete) is false. This also explains why (~Permissions.CanCreate).ToString() in the previous example returned a numeric value rather than the names of the included enumeration items. Thus, it is recommended to avoid using the NOT (~) operator to define composite enumeration items.
Change Log
- 2023-12-05 Initial version of the document created.